模块化编程的进化过程
原始写法
这种做法的缺点很明显:”污染”了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
1 | function m1() { |
对象写法
这样的写法会暴露所有模块成员,内部状态可以被外部改写。
1 | var module1 = new Object({ |
立即执行函数写法
使用”立即执行函数”(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。
1 | var module1 = (function () { |
JavaScript 模块的基本写法,之后对这种写法继续加工。
放大模式
继承其他模块,放大模式(augmentation)
1 | var module1 = (function (mod) { |
以上代码说明的是:module1 模块添加了一个新方法 m3(),然后返回新的 module1 模块
宽放大模式
有可能被继承的模块不存在,就会加载一个不存在的空对象,这时就要采用“款放大模式”
1 | var module1 = (function (mod) { |
输入全局变量
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
1 | var module1 = (function ($, YAHOO) { |
模块规范
CommonJS
编写模块
根据 CommonJS 规范,一个单独的文件就是一个模块,每一个模块都是一个单独的作用域,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。
通过exports
或者module.exports
来导出需要暴露的接口
加载模块
require()
函数用于加载模块:
1 | var math = require("math"); |
浏览器环境:加载模块会导致浏览器阻塞,所以要采用“异步加载”方式,也就是 AMD 规范诞生的背景。所以 CommonJS 更适合服务器环境。
评价
优点
- 服务器端模块便于重用
- NPM 中已经有将近 20 万个可以使用模块包
- 简单并容易使用
缺点
- 同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
- 不能非阻塞的并行加载多个模块
AMD
异步模块定义,AMD 是”Asynchronous Module Definition”的缩写.
定义模块
1 | define(id?, dependencies?, factory); |
- ID: 是一个字符串,表示模块标识。可以省略,则会定义一个匿名模块。
- dependencies: 是一个数组,成员是依赖模块的 id。可以省略,默认的依赖是:[“require”, “exports”, “module”]
- factory:是一个回调函数,在依赖的模块加载成功后,会执行这个回调函数
一般写法:
1 | define("adder", ["math"], function (math) { |
默认依赖:
1 | define("adder", function (require, exports) { |
匿名模块:
1 | define(["math"], function (math) { |
兼容 CommonJS(匿名+默认依赖):
1 | // module app/mime-client |
exports 写法
- 通过
exports
暴露接口
1 | define(function (require, exports) { |
- 通过
return
暴露接口
1 | define(function (require) { |
- 如果
return
是模块中唯一的代码,还可以写成这样:
1 | define({ |
- 错误的写法:
1 | define(function (require, exports) { |
- 正确的写法是用
return
或者给module.exports
赋值:
1 | define(function (require, exports, module) { |
提示:提示:exports
仅仅是module.exports
的一个引用。在factory
内部给exports
重新赋值时,并不会改变module.exports
的值。因此给exports
赋值是无效的,不能用来更改模块接口。
加载模块
有两个参数,当模块加载完之后,才调用回调函数,第一个参数是一个数组,成员就是要加载的模块
1 | require([module], callback); |
目前,主要有两个 Javascript 库实现了 AMD 规范:require.js 和 curl.js。
requireJS
RequireJS 的基本思想为:通过一个函数来将所有所需要的或者说所依赖的模块实现装载进来,然后返回一个新的函数(模块)。
贴上阮一峰的 requireJS 教程
引入 requireJS
- 普通方式:
1 | <script src="js/require.js"></script> |
- 异步加载方式(
async
属性表明这个文件需要异步加载,避免网页失去响应。IE 不支持这个属性,只支持defer
,所以把 defer 也写上):
1 | <script src="js/require.js" defer async="true"></script> |
- 最普遍的写法:
1 | <script data-main="scripts/main" src="scripts/require.js"></script> |
data-main
属性的作用是,指定网页程序的主模块。在上例中,就是 js 目录下面的main.js
,这个文件会第一个被require.js
加载。由于require.js
默认的文件后缀名是 js,所以可以把main.js
简写成 main。
main.js 的写法
require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
1 | // main.js |
代码含义:主模块的依赖模块是['moduleA', 'moduleB', 'moduleC']
。默认情况下,require.js
假定这三个模块与main.js
在同一个目录,文件名分别为moduleA.js
,moduleB.js
和moduleC.js
,然后自动加载。回调函数的参数只要位置对应即可,不一定要同名。
require.config() 自定义加载
require.config()就写在主模块(main.js)的头部,参数为一个对象。
1 | require.config({ |
加载非 AMD 规范的模块
举例来说,underscore 和 backbone 这两个库,都没有采用 AMD 规范编写。如果要加载它们的话,必须先定义它们的特征。
1 | require.config({ |
- exports 值(输出的变量名),表明这个模块外部调用时的名称;
- deps 数组,表明该模块的依赖性。
评价
优点
- 适合在浏览器环境中异步加载模块
- 可以并行加载多个模块
缺点
- 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
- 不符合通用的模块化思维方式,是一种妥协的实现
CMD
Common Module Definition
规范和AMD
很相似,尽量保持简单,并与CommonJS
和Node.js
的Modules
规范保持了很大的兼容性。在Sea.js
中实现。
官方文档:CMD 模块定义规范
定义模块
和 AMD 标准定义模块的方式一致,但有一些不同,主要表现在增加了一些对象:
define.cmd
:一个空对象,可用来判定当前页面是否有 CMD 模块加载器
1 | if (typeof define === "function" && define.cmd) { |
require.async(id, callback?)
用来在模块内部异步加载模块,并在加载完成后执行指定回调。
1 | define(function (require, exports, module) { |
require.resolve(id)
使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。
1 | define(function (require, exports) { |
评价
与 RequireJS 的 AMD 规范相比,CMD 规范尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。